﻿//--------------------------------------------------------------------------
// 
//  Copyright (c) Chili Software.  All rights reserved. 
// 
//  File: ObjectDataAdapter.cs
//
//  Description: Maps the data from the relations to objects and back.
// 
//--------------------------------------------------------------------------

using System;
using System.Data;
using System.Reflection;
using System.Collections.Generic;
using Chili.Opf3.Storages;
using Chili.Opf3.Query;
using Chili.Opf3.Query.OPathParser;
using Chili.Opf3.Mapping;

namespace Chili.Opf3.Storages
{
	/// <summary>
	/// Maps the data from the storage to the object and vice versa.
	/// </summary>
	/// <remarks>
	/// This class gets a DataReader that comes from ADO.NET and maps the content of that
	/// item to one single object that is also passed. The class maps also the properties of an
	/// object to an <see cref="Opf3.Query.ObjectExpression">ObjectExpression</see> that can
	/// then be used by the <see cref="Opf3.Storages.IStorage">IStorage</see> to save the
	/// object to the physical storage.
	/// </remarks>
	internal sealed class ObjectDataAdapter
	{
		private IStorage _storage;
		private ICustomDataTypesStorage _customDataTypesStorage;

		/// <summary>
		/// Creates a new instance of the <see cref="ObjectDataAdapter">ObjectDataAdapter
		/// Class</see>.
		/// </summary>
		/// <param name="storage">
		/// Storage that is associated with the
		/// <see cref="ObjectDataAdapter">ObjectDataAdapter</see> class. The storage is required to
		/// handle DBNulls that come from and go to the storage.
		/// </param>
		internal ObjectDataAdapter(IStorage storage)
		{
			// Don't check storage for null, because it's already checked in the ObjectContext 
			// and this constructor is internal.

			_storage = storage;
			_customDataTypesStorage = storage as ICustomDataTypesStorage;
		}

		#region FillObject

		/// <summary>Fills the object with the data from the current data record.</summary>
		/// <param name="type">The type of object that is populated with the data from the current item in the DataReader.</param>
		/// <param name="dataRecord">Data record that is used to populate the object.</param>
		/// <param name="schema">Contains the schema of the current ObjectReader instance.</param>
		public object FillObject(Type type, IDataRecord dataRecord, ObjectReaderSchema schema)
		{
			// Get the mapped members of the persistent objects.
			TypeMapping mapping = TypeMapping.GetTypeMapping(type);
			IMemberInfoCollection members = mapping.Members;

			// Create the args array.
			object[] args = new object[mapping.Members.Count];

			int count = members.Count;
			// Loop over all field in the current row of the data reader.
			for (int i = 0; i < count; i++)
			{
				IMemberInfo info = members[i];
				// Get the index of the field from the schema.
				int index = schema.IndexOfColumn(i);
				if (index == -1)
					continue;

				// Get the value.
				object value = dataRecord[i];
				// Check if the storage is a ICustomDataTypeStorage and transform the type form the storage
				// to the type of the persistent object.
				if (_customDataTypesStorage != null)
					value = _customDataTypesStorage.ChangeFromDataType(value, info.MemberType);
				// Convert the value from the storage from DbNull.
				if (value is DBNull)
					value = info.IsNullable ? null : _storage.ResolveStorageNullValue(value, info.MemberType);
				// If membertype is an enum then convert the value to enum.
				if (info.MemberType.IsEnum)
					value = Enum.ToObject(info.MemberType, value);

				args[i] = value;
			}

			return Activator.CreateInstance(type, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, null, args, null);
		}

		/// <summary>Fills the object with the data from the current data record.</summary>
		/// <param name="obj">Object that is populated with the data from the current item in the DataReader.</param>
		/// <param name="dataRecord">Data record that is used to populate the object.</param>
		/// <param name="objectInfo">The ObjectInfo object containing additional data on the object.</param>
		/// <param name="schema">Contains the schema of the current ObjectReader instance.</param>
		public void FillObject(object obj, IDataRecord dataRecord, ObjectInfo objectInfo, ObjectReaderSchema schema)
		{
			ChecksumBuilder builder = new ChecksumBuilder();

			// Get the mapped members of the persistent objects.
			TypeMapping mapping = TypeMapping.GetTypeMapping(obj);
			IMemberInfoCollection members = mapping.Members;

			int count = members.Count;
			// Loop over all field in the current row of the data reader.
			for (int i = 0; i < count; i++)
			{
				IMemberInfo info = members[i];
				// Get the index of the field from the schema.
				int index = schema.IndexOfColumn(i);
				if (index == -1)
					continue;

				// Fill one single member with data.
				FillMember(obj, info, dataRecord[index]);
				// Add the item to the checksum, if the persistent has more rights then only load.
				if (mapping.PersistentAttribute.Rights > PersistentRights.Load)
				{
					// Add the value to the builder.
					builder.AddMember(info.GetValue(obj), info.FieldAttribute);
				}
			}

			// Do IDynamicExtensible processing.
			IDynamicExtensible dynamicExtensible = obj as IDynamicExtensible;
			if (dynamicExtensible != null)
			{
				// Loop over all fields.
				for (int i = 0; i < dataRecord.FieldCount; i++)
				{
					string fieldName = dataRecord.GetName(i);
					IMemberInfo info = members.GetMemberInfoByField(fieldName);
					// If the field is mapped skip.
					if (info != null)
						continue;

					// Fill the dynamic extensible with data.
					object value = FillDynamicExtensible(dynamicExtensible, fieldName, dataRecord[i]);
					// Add the item to the checksum, if the persistent has more rights then only load.
					if (mapping.PersistentAttribute.Rights > PersistentRights.Load)
					{
						// Add the value to the builder.
						builder.AddMember(value, null);
					}
				}
			}

			// Create the checksum for the persistent object.
			objectInfo.Checksum = builder.ToChecksum();
		}

		/// <summary>
		/// Fills one single dynamic extensible of the persistent object with data.
		/// </summary>
		/// <param name="dynamicExtensible"><see cref="Chili.Opf3.IDynamicExtensible">IDynamicExtensible</see> that is populated with data.</param>
		/// <param name="fieldName">The name of the field that is associated with the item.</param>
		/// <param name="value">The value that is used to fill the object.</param>
		private object FillDynamicExtensible(IDynamicExtensible dynamicExtensible, string fieldName, object value)
		{
			// Inform the developer that a field in not mapped with a property.
			Tracing.Current.TraceVerbose("Field not mapped with property. Field name: " + fieldName);
			// Check if the storage is a ICustomDataTypeStorage and transform the type form the storage
			// to the type of the persistent object.
			if (_customDataTypesStorage != null && value != null)
				value = _customDataTypesStorage.ChangeFromDataType(value, value.GetType());

			dynamicExtensible.UnknownFields.Add(fieldName, value);

			return value;
		}

		/// <summary>
		/// Fills one single member of the persistent object with data.
		/// </summary>
		/// <param name="obj">Object that is populated with the data from the current item in the DataReader.</param>
		/// <param name="value">The value that is used to fill the object.</param>
		/// <param name="info">The member info object that is used to fill the member.</param>
		private void FillMember(object obj, IMemberInfo info, object value)
		{
			// Check if the storage is a ICustomDataTypeStorage and transform the type form the storage
			// to the type of the persistent object.
			if (_customDataTypesStorage != null)
				value = _customDataTypesStorage.ChangeFromDataType(value, info.MemberType);

			// Convert the value from the storage from DbNull.
			if (value is DBNull)
				value = info.IsNullable ? null : _storage.ResolveStorageNullValue(value, info.MemberType);

			info.SetValue(obj, value);
		}

		#endregion

		#region FillAutoNumber

		/// <summary>
		/// Fills the persistent object's
		/// <see cref="Opf3.FieldAttribute.AutoNumber">AutoNumber</see> property with the value
		/// from the storage.
		/// </summary>
		/// <param name="obj">
		/// Object that's first <see cref="Opf3.FieldAttribute.AutoNumber">AutoNumber</see>
		/// property is filled with the value.
		/// </param>
		/// <param name="value">Value that is filled in as autonumber value.</param>
		public void FillAutoNumber(object obj, object value)
		{
			if (value == null || value is DBNull)
			{
				Tracing.Current.TraceVerbose("FillAutoNumber method has been called, but value is null.");
				return;
			}

			Type type = obj.GetType();
			// Get the object storage mapping object.
			TypeMapping typeMapping = TypeMapping.GetTypeMapping(type);

			// Loop over all fields in the mapping and fill the parameter collection.
			foreach (IMemberInfo info in typeMapping.Members.GetIdentifierMemberMappings())
			{
				// Get the PropertyMapping of the current property.
				FieldAttribute attribute = info.FieldAttribute;

				if (attribute.AutoNumber)
				{
					// Convert and set the value. The storages sometimes don't return the appropriate
					// type when dealing with autonumbers.
					if (!info.IsUserDefinedType)
						value = Convert.ChangeType(value, info.MemberType);
					info.SetValue(obj, value);

					return;
				}
			}
		}

		#endregion
	}
}